﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Configuration;
using System.Linq;
using System.Web.Services;
using Facebook.Components;
using Facebook.Entity;
using GNUGo.Net.WebApp.Entities;
using GNUGo.Net.WebApp.WsClasses;
using System.Threading;
using System.Text;
using System.Net.Mail;
using System.Net;

namespace GNUGo.Net.WebApp {
	public class ReplayOnIgsParams {
		public int GameId { get; set; }
		public int PlayerId { get; set; }
	}

	public enum LogLevel {
		Debug = 1,
		Warn = 2,
		Error = 3
	}

	/// <summary>
	/// Summary description for GoServices
	/// </summary>
	[WebService(Namespace = "http://www.blueink.biz/SuperGo/GoServices.asmx")]
	[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
	[ToolboxItem(false)]
	// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
	// [System.Web.Script.Services.ScriptService]
	public class GoServices : WebService {
		private const string FACEBOOK_API_KEY = "46f49f2ca0c35480de66406438e6e2b5";
		private const string FACEBOOK_SECRET = "c5bac31a77d9ff826c26bbe65cfa6bf9";
		public const string SUPERGO_LINK = "<a href=\"http://apps.facebook.com/leesfirstfacebookapp/\">Super Go</a>";
		internal const string ENCDECPWD = "';Bki^D<:%Gc3.\vqQ6";

		private readonly FacebookService _fbService = new FacebookService();
		private readonly InternetGoServer _igs = new InternetGoServer();
		private static readonly Dictionary<int, string> _lastMoves = new Dictionary<int, string>(100);

		[WebMethod]
		public WsPlayer GetCurrentPlayer(string sessionKey, string userId) {
			try {
				GoEntitiesDataContext dataContext = GetDataContext();

				GoPlayer goPlayer = dataContext.GoPlayers.SingleOrDefault(p => p.FacebookUserId == userId);
				if (goPlayer != null)
					return new WsPlayer(goPlayer);

				User currentFacebookUser = GetFacebookCurrentUser(sessionKey, userId);

				goPlayer = new GoPlayer {
					FacebookUserId = userId,
					Name = currentFacebookUser.Name
				};

				dataContext.GoPlayers.InsertOnSubmit(goPlayer);
				dataContext.SubmitChanges();
				return new WsPlayer(goPlayer);
			} catch (Exception ex) {
				return new WsPlayer { FacebookUserId = userId, PlayerId = -1, Name = ex.ToString() };
			}
		}

		[WebMethod]
		public List<WsPlayer> GetGoFriends(string sessionKey, string userId) {
			try {
				Collection<User> friends = GetFacebookFriends(sessionKey, userId);
				IEnumerable<string> friendsUserIds = friends.Select(f => f.UserId);

				GoEntitiesDataContext dataContext = GetDataContext();

				IEnumerable<WsPlayer> friendsWithGoAccounts = dataContext.GoPlayers
					.Where(c => friendsUserIds.Contains(c.FacebookUserId) &&
						c.IgsUsername != null)
					.Select(goFriend =>
						new WsPlayer {
							Name = goFriend.Name,
							FacebookUserId = goFriend.FacebookUserId,
							PlayerId = goFriend.PlayerId
						});

				return friendsWithGoAccounts.ToList();
			} catch (Exception ex) {
				AddGoError("GetGoFriends userId=" + userId, ex.ToString());
				return new List<WsPlayer> { new WsPlayer { FacebookUserId = userId, PlayerId = -1, Name = ex.ToString() } };
			}
		}

		[WebMethod]
		public List<WsMove> AcceptGame(int gameId, WsPlayer currentUser, string session) {
			GoEntitiesDataContext context = GetDataContext();

			GoGame game = context.GoGames.Single(g => g.GameId == gameId);
			WsGame wsGame = new WsGame(game, currentUser.PlayerId);
			game.DateRequestAccepted = DateTime.Now;

			context.SubmitChanges();

			string message = String.Format("{0} has just accepted your game request on {1}.", currentUser.Name, SUPERGO_LINK);
			SendNotification(session, currentUser.FacebookUserId, message, wsGame.Opponent.FacebookUserId);

			return context.GoMoves
				.Where(m => m.GameId == gameId)
				.OrderBy(m => m.MoveNumber)
				.Select(m => new WsMove(m))
				.ToList();
		}

		[WebMethod]
		public List<WsGame> RejectGame(int gameId, WsPlayer currentUser, string session) {
			GoEntitiesDataContext context = GetDataContext();
			WsGame deletedGame = RejectGame(context, currentUser.PlayerId, gameId);

			string message = String.Format("{0} declined your {1} game request.", currentUser.Name, SUPERGO_LINK);
			SendNotification(session, currentUser.FacebookUserId, message, deletedGame.Opponent.FacebookUserId);

			return GetActiveGames(context, currentUser.PlayerId);
		}

		private static WsGame RejectGame(GoEntitiesDataContext context, int currentPlayerId, int gameId) {
			GoGame game = context.GoGames.Single(g => g.GameId == gameId);
			WsGame wsGame = new WsGame(game, currentPlayerId);

			// delete comments
			IQueryable<GoComment> comments = context.GoComments.Where(c => c.GoMove.GoGame == game);
			context.GoComments.DeleteAllOnSubmit(comments);

			// delete moves
			IQueryable<GoMove> moves = context.GoMoves.Where(m => m.GoGame == game);
			context.GoMoves.DeleteAllOnSubmit(moves);

			// delete observers
			IQueryable<GoObserver> observers = context.GoObservers.Where(o => o.GoGame == game);
			context.GoObservers.DeleteAllOnSubmit(observers);

			// delete the game
			context.GoGames.DeleteOnSubmit(game);

			context.SubmitChanges();

			return wsGame;
		}

		[WebMethod]
		public List<WsGame> WithdrawGame(int gameId, WsPlayer currentUser, string session) {
			GoEntitiesDataContext context = GetDataContext();
			WsGame deletedGame = RejectGame(context, currentUser.PlayerId, gameId);

			string message = String.Format("{0} has withdrawn their {1} game request.", currentUser.Name, SUPERGO_LINK);
			SendNotification(session, currentUser.FacebookUserId, message, deletedGame.Opponent.FacebookUserId);

			return GetActiveGames(context, currentUser.PlayerId);
		}

		[WebMethod]
		public WsGame CounterGame(WsGame counterOffer, WsPlayer currentUser, string session) {
			GoEntitiesDataContext context = GetDataContext();
			RejectGame(context, currentUser.PlayerId, counterOffer.GameId);
			WsGame newGame = CreateGame(context, counterOffer, currentUser);

			string message = String.Format("{0} countered your game request. Please log on to {1} to view updated terms.", currentUser.Name, SUPERGO_LINK);
			SendNotification(session, currentUser.FacebookUserId, message, newGame.Opponent.FacebookUserId);

			return newGame;
		}

		[WebMethod]
		public WsResult<WsPlayer> UpdateCurrentUser(WsPlayer currentWsPlayer) {
			GoEntitiesDataContext context = GetDataContext();

			bool userIsTryingToChangePassword = !String.IsNullOrEmpty(currentWsPlayer.IgsPassword);

			try {
				GoPlayer currentGoPlayer = context.GoPlayers.Single(p => p.PlayerId == currentWsPlayer.PlayerId);

				bool userIsTryingToChangeIgsUsernameOrPassword = userIsTryingToChangePassword ||
					currentGoPlayer.IgsUsername != currentWsPlayer.IgsUsername;

				if (userIsTryingToChangeIgsUsernameOrPassword) {
					// confirm you can connect to IGS with u/p
					InternetGoServer igs = new InternetGoServer();
					WsResult<object> igsCanLogInResult = igs.CanLogIn(currentWsPlayer.IgsUsername, currentWsPlayer.IgsPassword);
					if (!igsCanLogInResult.Success)
						return new WsResult<WsPlayer> {
							Success = false,
							ErrorMessage = igsCanLogInResult.ErrorMessage
						};
				}

				currentGoPlayer.IgsUsername = currentWsPlayer.IgsUsername;
				currentGoPlayer.NotificationEmail = currentWsPlayer.Email;

				if (userIsTryingToChangePassword) {
					string encryptedPassword = EncDec.Encrypt(currentWsPlayer.IgsPassword, ENCDECPWD, currentGoPlayer.PlayerId);
					currentGoPlayer.IgsPassword = encryptedPassword;
				}

				context.SubmitChanges();

				return new WsResult<WsPlayer> {
					Success = true,
					Result = new WsPlayer(currentGoPlayer)
				};
			} catch (Exception ex) {
				AddGoError("UpdateCurrentUser", ex + "\r\n\r\n" + currentWsPlayer, currentWsPlayer.PlayerId);
				return new WsResult<WsPlayer> {
					Success = false,
					ErrorMessage = "An unexpected error occurred. " + ex
				};
			}
		}

		[WebMethod]
		public List<WsGame> GetActiveGames(int playerId) {
			GoEntitiesDataContext dataContext = GetDataContext();
			return GetActiveGames(dataContext, playerId);
		}

		private static List<WsGame> GetActiveGames(GoEntitiesDataContext dataContext, int playerId) {
			var activeGames = from g in dataContext.GoGames
							  join wp in dataContext.GoPlayers on g.WhitePlayerId equals wp.PlayerId
							  join bp in dataContext.GoPlayers on g.BlackPlayerId equals bp.PlayerId
							  where (g.WhitePlayerId == playerId || g.BlackPlayerId == playerId) &&
							  g.DateFinished == null
							  select new {
								  Game = g,
								  WhitePlayer = wp,
								  BlackPlayer = bp
							  };

			return activeGames.Select(game => new WsGame(game.Game, game.BlackPlayer, game.WhitePlayer, playerId)).ToList();
		}

		[WebMethod]
		public List<WsMove> GetMovesOfGameSorted(int gameId) {
			GoEntitiesDataContext dataContext = GetDataContext();

			return dataContext.GoMoves
				.Where(m => m.GameId == gameId)
				.OrderBy(m => m.MoveNumber)
				.Select(m => new WsMove(m))
				.ToList();
		}

		[WebMethod]
		public List<WsMove> GetDeadGroups(int gameId) {
			GoEntitiesDataContext dataContext = GetDataContext();

			return dataContext.GoDeadGroups
				.Where(d => d.GameId == gameId)
				.Select(d => new WsMove(d))
				.ToList();
		}

		[WebMethod]
		public string GetLastMove(int gameId) {
			string result;
			if (_lastMoves.TryGetValue(gameId, out result))
				return result;
			
			// This line shouldn't hit very often.  Only if a game is finished or the cache
			// was reset for some reason.  We need to differentiate.
			// 
			// A given game's last move entry is set to null when the game is finalized.
			// The opponent might thus ask for the last move after it's finalized and get
			// here, but they shouldn't poll again, so we can justify a more expensive
			// database hit to verify that the cache wasn't reset.

			GoEntitiesDataContext dataContext = GetDataContext();
			GoGame goGame = dataContext.GoGames.SingleOrDefault(g => g.GameId == gameId);
			if (goGame == null) {
				AddGoError(gameId, null, "GetLastMove Request", "Invalid gameId requested", null, dataContext, LogLevel.Error);
				return null;
			}
			
			// if the game was actually finished return null, the cache doesn't need to be rebuilt
			if (goGame.DateFinished != null)
				return null;

			// if the game wasn't started return null (this shouldn't really ever happen)
			if (goGame.CurrentMoveNumber == 0)
				return null;

			AddGoError(gameId, null, "Rebuilding Cache Initiated by GameId " + gameId, "Rebuilding Cache.", null, dataContext, LogLevel.Warn);
			RebuildLastMoveCache(dataContext);
			if (_lastMoves.TryGetValue(gameId, out result))
				return result;

			AddGoError(gameId, null, "GetLastMove Request", "After rebuilding the cache the value of a game was still null.", null, dataContext, LogLevel.Error);
			throw new Exception("After rebuilding the cache the value of a game was still null.");
		}

		private static void RebuildLastMoveCache(GoEntitiesDataContext dataContext) {
			IQueryable<GoGame> unfinishedGames = dataContext.GoGames.Where(g => g.DateFinished == null);
			foreach (GoGame game in unfinishedGames) {
				int gameId = game.GameId;
				int currentPlayerId = game.BlackToMove ? game.BlackPlayerId : game.WhitePlayerId;
				
				// accept dead
				if (game.DateBlackAcceptedDeadGroups != null || game.DateWhiteAcceptedDeadGroups != null) {
					AddLastMove(MoveType.AcceptDead, gameId, currentPlayerId);
					continue;
				}

				// mark dead
				if (game.GoDeadGroups.Count > 0) {
					AddLastMove(MoveType.MarkDead, gameId, currentPlayerId);
					continue;
				}

				// two passes occurred
				if (game.DateTwoPassesOccurred != null) {
					AddLastMove(MoveType.TwoPassesOccurred, gameId, currentPlayerId);
					continue;
				}

				int maxMoveNumber;
				if (IsLastMovePass(dataContext, game, out maxMoveNumber)) {
					AddLastMove(MoveType.Pass, gameId, game.CurrentMoveNumber, currentPlayerId);
					continue;
				}

				// if the game was just initiated
				if (game.GoMoves.Count == 0)
					continue;

				GoMove lastMove = dataContext.GoMoves
					.SingleOrDefault(m => m.GameId == gameId && m.MoveNumber == maxMoveNumber);
				if (lastMove == null)
					throw new Exception("Unable to find maxMove for gameId " + gameId + " where maxMoveNumber = " + maxMoveNumber);
				AddLastMove(MoveType.Move, gameId, game.CurrentMoveNumber, lastMove.X, lastMove.Y, currentPlayerId);
			}
		}

		private static void AddLastMove(MoveType moveType, int gameId, int moveNumber, int x, int y, int currentPlayerId) {
			_lastMoves[gameId] = currentPlayerId + " " + GetMoveTypeChar(moveType) + " " + moveNumber + " " + x + " " + y;
		}

		private static void AddLastMove(MoveType moveType, int gameId, int moveNumber, int currentPlayerId) {
			_lastMoves[gameId] = currentPlayerId + " " + GetMoveTypeChar(moveType) + " " + moveNumber;
		}

		private static void AddLastMove(MoveType moveType, int gameId, int currentPlayerId) {
			if (moveType == MoveType.Finalize) {
				_lastMoves.Remove(gameId);
				return;
			}

			_lastMoves[gameId] = currentPlayerId + " " + GetMoveTypeChar(moveType);
		}

		private static string GetMoveTypeChar(MoveType moveType) {
			switch (moveType) {
				case MoveType.Move:
					return "M";
				case MoveType.Pass:
					return "P";
				case MoveType.MarkDead:
					return "D";
				case MoveType.AcceptDead:
					return "A";
				case MoveType.TwoPassesOccurred:
					return "T";
				default:
					throw new Exception("Unknown MoveType");
			}
		}

		private enum MoveType {
			Move,
			Pass,
			TwoPassesOccurred,
			MarkDead,
			AcceptDead,
			Finalize
		}

		[WebMethod]
		public int AddMove(int gameId, int x, int y, bool isBlack, int captures, string session) {
			GoEntitiesDataContext dataContext = GetDataContext();

			GoGame game = dataContext.GoGames.SingleOrDefault(g => g.GameId == gameId);

			if (game == null)
				throw new Exception("gameId '" + gameId + "' is not a valid game.");

			GoMove move = new GoMove {
				X = x,
				Y = y,
				MoveNumber = game.CurrentMoveNumber,
				GoGame = game
			};

			dataContext.GoMoves.InsertOnSubmit(move);
			game.CurrentMoveNumber++;
			game.BlackToMove = !game.BlackToMove;

			dataContext.SubmitChanges();

			GoPlayer justMoved = isBlack ? game.GoPlayerBlack : game.GoPlayerWhite;
			GoPlayer opponent = isBlack ? game.GoPlayerWhite : game.GoPlayerBlack;

			AddLastMove(MoveType.Move, gameId, game.CurrentMoveNumber, x, y, justMoved.PlayerId);

			string message = String.Format("{0} has moved in game #{1} of {2}.", justMoved.Name, game.GameId, SUPERGO_LINK);
			SendNotification(session, justMoved.FacebookUserId, message, opponent.FacebookUserId);

			return move.MoveId;
		}

		[WebMethod]
		public void AddPass(int gameId, bool isBlack, string session) {
			GoEntitiesDataContext dataContext = GetDataContext();

			GoGame game = dataContext.GoGames.SingleOrDefault(g => g.GameId == gameId);

			if (game == null)
				throw new Exception("gameId '" + gameId + "' is not a valid game.");

			bool lastMoveWasPass = IsLastMovePass(dataContext, game);
			if (lastMoveWasPass)
				game.DateTwoPassesOccurred = DateTime.Now;

			game.CurrentMoveNumber++;
			if (!lastMoveWasPass)
				game.BlackToMove = !game.BlackToMove;
			dataContext.SubmitChanges();

			GoPlayer justMoved = isBlack ? game.GoPlayerBlack : game.GoPlayerWhite;
			GoPlayer opponent = isBlack ? game.GoPlayerWhite : game.GoPlayerBlack;

			AddLastMove(lastMoveWasPass ? MoveType.TwoPassesOccurred : MoveType.Pass, gameId, game.CurrentMoveNumber, justMoved.PlayerId);

			string message = String.Format("{0} passed in game #{1} of {2}.", justMoved.Name, game.GameId, SUPERGO_LINK);
			if (lastMoveWasPass)
				message += "  This is the second concurrent pass, so your opponent will now mark dead groups after which you can approve/reject.";

			SendNotification(session, justMoved.FacebookUserId, message, opponent.FacebookUserId);
		}

		private static bool IsLastMovePass(GoEntitiesDataContext dataContext, GoGame game) {
			int maxMoveNumber;
			return IsLastMovePass(dataContext, game, out maxMoveNumber);
		}

		private static bool IsLastMovePass(GoEntitiesDataContext dataContext, GoGame game, out int maxMoveNumber) {
			maxMoveNumber = dataContext.GoMoves
				.Where(m => m.GoGame == game)
				.Max(m => m.MoveNumber);
			return maxMoveNumber + 1 != game.CurrentMoveNumber;
		}

		[WebMethod]
		public void AddDeadGroup(int gameId, int x, int y, int stonesInGroup, WsPlayer currentUser) {
			GoEntitiesDataContext dataContext = GetDataContext();

			GoGame game = dataContext.GoGames.SingleOrDefault(g => g.GameId == gameId);

			// if a dead group is added or removed then clear everyone's acceptance
			game.DateBlackAcceptedDeadGroups = null;
			game.DateWhiteAcceptedDeadGroups = null;

			GoDeadGroup existingDeadGroup = dataContext.GoDeadGroups.SingleOrDefault(g => g.X == x && g.Y == y && g.GoGame == game);

			// if the group already exists then delete it otherwise add it (like a toggle)
			if (existingDeadGroup != null) {
				dataContext.GoDeadGroups.DeleteOnSubmit(existingDeadGroup);
			} else {
				game.GoDeadGroups.Add(
					new GoDeadGroup {
						GoGame = game,
						X = x,
						Y = y,
						StonesInGroup = stonesInGroup
					}
				);
			}

			dataContext.SubmitChanges();

			AddLastMove(MoveType.MarkDead, gameId, game.CurrentMoveNumber, x, y, currentUser.PlayerId);
		}

		[WebMethod]
		public void AcceptDeadGroups(int gameId, WsPlayer currentUser, string session) {
			GoEntitiesDataContext dataContext = GetDataContext();

			GoGame game = dataContext.GoGames.SingleOrDefault(g => g.GameId == gameId);

			GoPlayer opponent;
			GoPlayer justMoved;
			GetOpponentAndJustMoved(game, currentUser, out justMoved, out opponent);

			if (PlayerIsBlack(game, currentUser)) {
				game.DateBlackAcceptedDeadGroups = DateTime.Now;
			} else if (PlayerIsWhite(game, currentUser)) {
				game.DateWhiteAcceptedDeadGroups = DateTime.Now;
			} else {
				throw new Exception("Non players can't accept dead groups.");
			}

			game.BlackToMove = !game.BlackToMove;

			bool isDeadMarkingComplete = game.DateWhiteAcceptedDeadGroups != null && game.DateBlackAcceptedDeadGroups != null;
			if (isDeadMarkingComplete)
				throw new Exception("FinalizeGame() should be called not AcceptDeadGroups(), because the opponent has already acepted all dead groups.");

			dataContext.SubmitChanges();

			string message = String.Format("{0} has identified dead groups in game #{1}. Please log in to {2} to confirm the state of the board.",
										   justMoved.Name, game.GameId, SUPERGO_LINK);
			SendNotification(session, justMoved.FacebookUserId, message, opponent.FacebookUserId);

			AddLastMove(MoveType.AcceptDead, gameId, currentUser.PlayerId);
		}

		/// <summary>
		/// This method should be called after the opponent has accepted dead groups to indicate
		/// the current user agrees to his opponent's board state.  Since there is an acceptance
		/// the final score should be submitted and this will replay the game on IGS.
		/// </summary>
		[WebMethod]
		public string FinalizeGame(int gameId, decimal finalScoreBlack, decimal finalScoreWhite, int blackCaptures, int whiteCaptures, WsPlayer currentUser, string session, bool replayOnIgsAsync) {
			GoEntitiesDataContext dataContext = GetDataContext();

			GoGame game = dataContext.GoGames.SingleOrDefault(g => g.GameId == gameId);

			GoPlayer opponent;
			GoPlayer justMoved;
			GetOpponentAndJustMoved(game, currentUser, out justMoved, out opponent);

			bool hasOpponentAcceptedDeadGroups;
			if (PlayerIsBlack(game, currentUser)) {
				hasOpponentAcceptedDeadGroups = game.DateWhiteAcceptedDeadGroups != null;
				game.DateBlackAcceptedDeadGroups = DateTime.Now;
			} else if (PlayerIsWhite(game, currentUser)) {
				hasOpponentAcceptedDeadGroups = game.DateBlackAcceptedDeadGroups != null;
				game.DateWhiteAcceptedDeadGroups = DateTime.Now;
			} else {
				throw new Exception("Only players can finalize game.");
			}

			if (!hasOpponentAcceptedDeadGroups)
				throw new Exception("Can't finalize a game when the opponent hasn't accepted dead groups.");

			game.DateFinished = DateTime.Now;
			game.FinalScoreBlack = finalScoreBlack;
			game.FinalScoreWhite = finalScoreWhite;
			game.BlackCaptures = blackCaptures;
			game.WhiteCaptures = whiteCaptures;
			game.BlackToMove = false;

			dataContext.SubmitChanges();

			AddLastMove(MoveType.Finalize, gameId, currentUser.PlayerId);

			string opponentMessage;
			string currentUserMessage;
			GetNotificationMessages(game, finalScoreBlack, finalScoreWhite, justMoved, opponent, out opponentMessage, out currentUserMessage);

			SendNotification(session, justMoved.FacebookUserId, opponentMessage, opponent.FacebookUserId);

			if (replayOnIgsAsync)
				ReplayOnIgsAsync(new ReplayOnIgsParams {
					GameId = gameId,
					PlayerId = currentUser.PlayerId
				});

			return currentUserMessage;
		}

		private void ReplayOnIgsAsync(ReplayOnIgsParams replayOnIgsParams) {
			Thread t = new Thread(new ParameterizedThreadStart(ReplayOnIgs)) {
				IsBackground = true,
				Name = "ReplayOnIgsThread"
			};
			t.Start(replayOnIgsParams);
		}

		private void ReplayOnIgs(object replayOnIgsParamsObj) {
			GoGame game;
			ReplayOnIgsParams replayOnIgsParams = (ReplayOnIgsParams)replayOnIgsParamsObj;
			int gameId = replayOnIgsParams.GameId;
			int playerId = replayOnIgsParams.PlayerId;

			GoEntitiesDataContext dataContext = GetDataContext();
			StringBuilder sbLog = new StringBuilder();

			try {
				_igs.DebugLog = sbLog;
				_igs.PlayGoGame(gameId);

				game = dataContext.GoGames.SingleOrDefault(g => g.GameId == gameId);
				game.DateSubmittedToIGS = DateTime.Now;
				if (game.FinalScoreBlack != _igs.FinalScoreBlack || game.FinalScoreWhite != _igs.FinalScoreWhite)
					throw new Exception(String.Format("Final scores did not match with IGS.  _igs.FinalScoreBlack = {0}, _igs.FinalScoreWhite = {1}, game.FinalScoreBlack = {2}, game.FinalScoreWhite = {3}",
						_igs.FinalScoreBlack,
						_igs.FinalScoreWhite,
						game.FinalScoreBlack,
						game.FinalScoreWhite)
						);

				if (ErrorLevel == (int)LogLevel.Debug) {
					AddGoError(gameId, playerId, "Replaying Game on IGS", sbLog.ToString(), null, dataContext, LogLevel.Debug);
				}
				dataContext.SubmitChanges();
			} catch (Exception ex) {
				AddGoError(gameId, playerId, "Replaying Game on IGS", ex.ToString(), sbLog.ToString(), dataContext, LogLevel.Error);
			}
		}

		public void AddGoError(
			string activity,
			string message,
			int playerId
			) {

			AddGoError(null, playerId, activity, message, null, null, LogLevel.Error);
		}

		public void AddGoError(
			string activity,
			string message
			) {

			AddGoError(null, null, activity, message, null, null, LogLevel.Error);
		}

		public void AddGoDebug(string message) {
			AddGoError(null, null, null, message, null, null, LogLevel.Debug);
		}

		public static string MergeString(string s1, string s2, int maxLen) {
			if (String.IsNullOrEmpty(s2))
				return Last(s1, maxLen);
			if (s1.Length + s2.Length <= maxLen)
				return s1 + s2;
			if (s1.Length > maxLen)
				return Last(s1, maxLen);
			int charsAvailable = maxLen - s1.Length;
			return s1 + Last(s2, charsAvailable);
		}

		public static string Last(string s, int maxLen) {
			if (s == null)
				return null;
			if (s.Length <= maxLen)
				return s;
			return s.Substring(s.Length - maxLen);
		}

		private static string First(string s, int len) {
			if (s == null) return null;
			return s.Length <= len ? s : s.Remove(len);
		}

		public void AddGoError(
			int? gameId,
			int? playerId,
			string activity,
			string message,
			string logInfo,
			GoEntitiesDataContext dataContext,
			LogLevel logLevel) {

			if (dataContext == null)
				dataContext = GetDataContext();

			// don't log errors less than the current level
			if ((int)logLevel < ErrorLevel)
				return;

			GoError goError = GetGoError(gameId, playerId, activity, message, logInfo, (int)logLevel);
			dataContext.GoErrors.InsertOnSubmit(goError);
			dataContext.SubmitChanges();

			// only send e-mail for errors
			if (logLevel != LogLevel.Error) return;
			// only send e-mail in production
			if (InDevelopment()) return;

			SmtpClient smtpClient = new SmtpClient("mail.blueink.biz");
			MailMessage mailMessage = new MailMessage {
				Subject = ("Super Go Error #" + goError.ErrorId),
				IsBodyHtml = false,
				Body = message + "\r\n\r\n" + logInfo,
				From = new MailAddress("supergo@blueink.biz", "Super Go"),
				Sender = new MailAddress("supergo@blueink.biz", "Super Go")
			};
			mailMessage.To.Add(new MailAddress("supergoerrors@leerichardson.com", "Super Go Errors"));
			smtpClient.UseDefaultCredentials = false;
			smtpClient.Credentials = new NetworkCredential("supergo@blueink.biz", "p~$$W0rd!");
			smtpClient.Send(mailMessage);
		}

		private static GoError GetGoError(int? gameId, int? playerId, string activity, string message, string logInfo, int debugLevel) {
			string threadName = First(Thread.CurrentThread.Name, 100) ?? "[None]";
			

			return new GoError {
				Activity = First(activity, 100),
				ErrorDate = DateTime.Now,
				ErrorLevelId = debugLevel,
				GameId = gameId,
				PlayerId = playerId,
				ThreadName = threadName,
				Message = MergeString(message, logInfo, 5000)
			};
		}

		private static void GetNotificationMessages(GoGame game, decimal finalScoreBlack, decimal finalScoreWhite, GoPlayer justMoved, GoPlayer opponent, out string opponentMessage, out string currentUserMessage) {
			opponentMessage = String.Format("{0} accepted dead groups in game #{1} of {2} thus finishing the game.  The final result is ",
									justMoved.Name, game.GameId, SUPERGO_LINK);

			bool tieGame = finalScoreBlack == finalScoreWhite;
			bool blackWon = finalScoreBlack > finalScoreWhite;
			bool opponentIsBlack = opponent == game.GoPlayerBlack;
			bool opponentWon = blackWon == opponentIsBlack;
			decimal winningScore = blackWon ? finalScoreBlack : finalScoreWhite;
			decimal losingScore = blackWon ? finalScoreWhite : finalScoreBlack;

			if (tieGame) {
				opponentMessage += String.Format("it was a tie {0} to {0}.", finalScoreBlack);
				currentUserMessage = String.Format("It was a tie {0:#.#} to {0:#.#}", finalScoreBlack);
			} else if (opponentWon) {
				opponentMessage += String.Format("you won {0} to {1}.", winningScore, losingScore);
				currentUserMessage = String.Format("Unfortunately, {0} won {1:#.#} to {2:#.#}.", opponent.Name, winningScore, losingScore);
			} else {
				opponentMessage += String.Format("{0} won {1} to {2}.", opponent.Name, winningScore, losingScore);
				currentUserMessage = String.Format("Congratulations, you won against {0} by {1:#.#} to {2:#.#}.", opponent.Name, winningScore, losingScore);
			}

			opponentMessage += String.Format(" Would you like to play another game of {0}?", SUPERGO_LINK);
		}

		[WebMethod]
		public void Resign(int gameId, WsPlayer currentUser, string session) {
			GoEntitiesDataContext dataContext = GetDataContext();

			GoGame game = dataContext.GoGames.SingleOrDefault(g => g.GameId == gameId);

			GoPlayer opponent;
			GoPlayer justMoved;
			GetOpponentAndJustMoved(game, currentUser, out justMoved, out opponent);

			if (PlayerIsBlack(game, currentUser))
				game.DateBlackResigned = DateTime.Now;
			else if (PlayerIsWhite(game, currentUser))
				game.DateWhiteResigned = DateTime.Now;
			else
				throw new Exception("Non players can't accept dead groups.");
			game.DateFinished = DateTime.Now;

			dataContext.SubmitChanges();

			string message = String.Format("{0} has just resigned in game #{1} of {2}.  So you won.  Would you like to play again?",
										   justMoved.Name, game.GameId, SUPERGO_LINK);
			SendNotification(session, justMoved.FacebookUserId, message, opponent.FacebookUserId);
		}

		private static bool PlayerIsBlack(GoGame game, WsPlayer player) {
			return game.BlackPlayerId == player.PlayerId;
		}

		private static bool PlayerIsWhite(GoGame game, WsPlayer player) {
			return game.WhitePlayerId == player.PlayerId;
		}

		[WebMethod]
		public WsGame CreateGame(WsGame gameToInitiate, WsPlayer currentUser, string session) {
			GoEntitiesDataContext dataContext = GetDataContext();
			WsGame result = CreateGame(dataContext, gameToInitiate, currentUser);

			string message = String.Format("{0} has just challanged you to a game of {1}.", currentUser.Name, SUPERGO_LINK);
			SendNotification(session, currentUser.FacebookUserId, message, gameToInitiate.Opponent.FacebookUserId);

			return result;
		}

		private static WsGame CreateGame(GoEntitiesDataContext dataContext, WsGame gameToInitiate, WsPlayer currentUser) {
			GoGame goGame = new GoGame();
			if (gameToInitiate.OpponentIsBlack) {
				goGame.BlackPlayerId = gameToInitiate.Opponent.PlayerId;
				goGame.WhitePlayerId = currentUser.PlayerId;
			} else {
				goGame.BlackPlayerId = currentUser.PlayerId;
				goGame.WhitePlayerId = gameToInitiate.Opponent.PlayerId;
			}

			goGame.DateRequestMade = DateTime.Now;
			goGame.RequestInitiatedByBlack = gameToInitiate.RequestInitiatedByBlack;
			goGame.BlackHandicap = gameToInitiate.BlackHandicap;
			goGame.BlackToMove = gameToInitiate.BlackHandicap == 0;
			goGame.Komi = gameToInitiate.Komi;
			goGame.BoardSize = gameToInitiate.BoardSize;
			goGame.CurrentMoveNumber = 0;
			goGame.BlackCaptures = 0;
			goGame.WhiteCaptures = 0;

			dataContext.GoGames.InsertOnSubmit(goGame);
			dataContext.SubmitChanges();

			return new WsGame(goGame, currentUser.PlayerId);
		}

		private static int _debugLevel = -1;

		public int ErrorLevel {
			get {
				if (_debugLevel != -1)
					return _debugLevel;
				GoEntitiesDataContext dataContext = GetDataContext();
				GoConfig debugLevel = dataContext.GoConfigs.SingleOrDefault(c => c.Name == "DebugLevel");
				if (debugLevel == null) {
					Initialize();
					debugLevel = dataContext.GoConfigs.SingleOrDefault(c => c.Name == "DebugLevel");
				}
				_debugLevel = Convert.ToInt32(debugLevel.Value);
				return _debugLevel;
			}
			set {
				GoEntitiesDataContext dataContext = GetDataContext();
				GoConfig debugLevel = dataContext.GoConfigs.SingleOrDefault(c => c.Name == "DebugLevel");
				if (debugLevel == null) {
					Initialize();
					debugLevel = dataContext.GoConfigs.SingleOrDefault(c => c.Name == "DebugLevel");
				}
				debugLevel.Value = value.ToString();
				dataContext.SubmitChanges();
				_debugLevel = value;
			}
		}

		private static void ResetCache() {
			_debugLevel = -1;
			_lastMoves.Clear();
		}

		[WebMethod]
		public void Initialize() {
			GoEntitiesDataContext dataContext = GetDataContext();
			ResetCache();

			if (dataContext.GoConfigs.Count() != 0) return;

			GoConfig goConfig = new GoConfig { Name = "DebugLevel", Value = "2" };
			dataContext.GoConfigs.InsertOnSubmit(goConfig);

			dataContext.GoErrorLevels.InsertOnSubmit(new GoErrorLevel { ErrorLevelId = (int)LogLevel.Debug, ErrorLevel = "Debug" });
			dataContext.GoErrorLevels.InsertOnSubmit(new GoErrorLevel { ErrorLevelId = (int)LogLevel.Warn, ErrorLevel = "Warn" });
			dataContext.GoErrorLevels.InsertOnSubmit(new GoErrorLevel { ErrorLevelId = (int)LogLevel.Error, ErrorLevel = "Error" });

			dataContext.SubmitChanges();
		}

		private static void GetOpponentAndJustMoved(GoGame game, WsPlayer currentUser, out GoPlayer justMoved, out GoPlayer opponent) {
			if (PlayerIsBlack(game, currentUser)) {
				opponent = game.GoPlayerWhite;
				justMoved = game.GoPlayerBlack;
			} else if (PlayerIsWhite(game, currentUser)) {
				opponent = game.GoPlayerBlack;
				justMoved = game.GoPlayerWhite;
			} else {
				throw new Exception("Non players can't modify the board state.");
			}
		}

		internal static GoEntitiesDataContext GetDataContext() {
			ConnectionStringSettings connectionStringSetting = ConfigurationManager.ConnectionStrings["GoConnectionString"];
			if (connectionStringSetting == null)
				throw new Exception("Expected a ConnectionString setting named 'GoConnectionString'");
			string connectionString = connectionStringSetting.ConnectionString;
			return new GoEntitiesDataContext(connectionString);
		}

		private static bool InDevelopment() {
			//return Context.Request.Url.Host == "localhost";
			string inDevelopment = ConfigurationManager.AppSettings["InDevelopment"];
			if (String.IsNullOrEmpty(inDevelopment))
				throw new Exception("The configuration files needs an app setting called 'InDevelopment' that must be set to 'true' or 'false'.  Among other things this setting will determine whether to connect to the Facebook service.");
			return inDevelopment.ToLower() == "true";
		}

		private void SendNotification(string sessionKey, string userId, string message, string toList) {
			if (InDevelopment())
				return;

			InitializerFacebookService(sessionKey, userId);
			_fbService.SendNotification(message, toList);
		}

		private User GetFacebookCurrentUser(string sessionKey, string userId) {
			if (InDevelopment())
				return GetFakeCurrentUser(userId);

			InitializerFacebookService(sessionKey, userId);
			return _fbService.GetUserInfo();
		}

		private static User GetFakeCurrentUser(string userId) {
			switch (userId) {
				case "lprichar":
					return new User { UserId = "lprichar", Name = "Lee Richardson" };
				case "jferner":
					return new User { UserId = "jferner", Name = "Joe Ferner" };
				case "jjohnson":
					return new User { UserId = "jjohnson", Name = "John Johnson" };
				default:
					return new User { UserId = userId, Name = "Anonymous" };
			}
		}

		private void InitializerFacebookService(string sessionKey, string userId) {
			if (String.IsNullOrEmpty(sessionKey) || String.IsNullOrEmpty(userId)) {
				throw new InvalidOperationException("Not logged on to facebook.");
			}

			_fbService.ApplicationKey = FACEBOOK_API_KEY;
			_fbService.Secret = FACEBOOK_SECRET;
			_fbService.IsDesktopApplication = false;
			_fbService.SessionKey = sessionKey;
			_fbService.UserId = userId;
		}

		private Collection<User> GetFacebookFriends(string sessionKey, string userId) {
			// if we're in development return some fake data
			if (InDevelopment()) {
				return GetFakeFriends(userId);
			}

			// else if we're in production set up the facebook service
			InitializerFacebookService(sessionKey, userId);
			return _fbService.GetFriends();
		}

		private static Collection<User> GetFakeFriends(string userId) {
			switch (userId) {
				case "lprichar":
					return new Collection<User> {
							new User { Name = "John Johnson", UserId = "jjohnson" },
							new User { Name = "Joseph Ferner", UserId = "jferner" },
						};
				case "jferner":
					return new Collection<User> {
							new User { Name = "Lee Richardson", UserId = "lprichar" },
						};
				case "jjohnson":
					return new Collection<User> {
							new User { Name = "Lee Richardson", UserId = "lprichar" },
						};
				default:
					return new Collection<User> {
							new User { Name = "Lee Richardson", UserId = "lprichar" },
						};
			}
		}
	}
}
